package chagine.core.lock.redis;

import chagine.core.lock.base.AbstractLock;
import chagine.core.lock.redis.util.LockInfo;
import chagine.core.lock.redis.util.SeeingLockRedisUtil;
import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;

/**
 * 基于Redis的SETNX操作实现的分布式锁, 获取锁时最好用tryLock(long time, TimeUnit unit), 以免网路问题而导致线程一直阻塞.
 * <a href="http://redis.io/commands/setnx">SETNC操作参考资料.</a>
 * <p>
 * <p><b>可重入实现关键:</b>
 * <ul>
 * <li>在分布式环境中如何确定一个线程? <i><b>mac地址 + jvm pid + threadId</b></i> (mac地址唯一, jvm
 * pid在单机内唯一, threadId在单jvm内唯一)</li>
 * <li>任何一个线程从redis拿到value值后都需要能确定 该锁是否被自己持有, 因此value值要有以下特性: 保存持有锁的主机(mac), jvm
 * pid, 持有锁的线程ID, 重复持有锁的次数</li>
 * </ul></p>
 * <p>
 * redis中value设计如下(in json):
 * <pre>
 * {
 * 	expires : expire time in long
 * 	mac : mac address of lock holder's machine
 * 	pid : jvm process id
 * 	threadId : lock holder thread id
 * 	count : hold count(for use of reentrancy)
 * }
 * 由{@link LockInfo LockInfo}表示.
 * </pre>
 * <p>
 * <b>Usage Example:</b>
 * <pre>
 *    {@link Lock} lock = new {@link RedisReentrantLock}(seeingLockRedisUtil, "lockKey", lockExpires, timeServerAddr);
 * 	if (lock.tryLock(3, TimeUnit.SECONDS)) {
 * 		try {
 * 			// do something
 *        } catch (Exception e) {
 * 			lock.unlock();
 *        }
 *    }
 * </pre>
 * </p>
 *
 * @author lixiaohui
 * @date 2016年9月15日 下午2:52:38
 */
@Slf4j
public class RedisReentrantLock extends AbstractLock {

    // 默认锁的有效时间，单位 millis
    protected static final long DEFAULT_LOCK_EXPIRES = 3000L;

    private final SeeingLockRedisUtil seeingLockRedisUtil;

    private final String lockKey;

    // 锁的有效时长(毫秒)
    private final long lockExpires;

    // 拓展连接不同的redis DB
    public RedisReentrantLock(String lockKey, long lockExpires, SeeingLockRedisUtil seeingLockRedisUtil) {
        this.lockKey = "Lock_" + lockKey;
        this.lockExpires = (lockExpires <= 0) ? DEFAULT_LOCK_EXPIRES : lockExpires;
        this.seeingLockRedisUtil = seeingLockRedisUtil;
    }

    // 阻塞式获取锁的实现
    @Override
    protected boolean lock(boolean useTimeout, long time, TimeUnit unit, boolean interrupt) throws InterruptedException {
        if (interrupt) {
            checkInterruption();
        }

        // 超时控制 的时间可以从本地获取, 因为这个和锁超时没有关系, 只是一段时间区间的控制
        long start = localTimeMillis();
        // if !useTimeout, then it's useless
        // 默认为秒的单位
        long timeout = time * 1000;
        if (useTimeout && unit != null) {
            timeout = unit.toMillis(time);
        }

        // 1. lockKey未关联value, 直接设置lockKey, 成功获取到锁, return true
        // 2. lock 已过期, 用getset设置lockKey, 判断返回的旧的LockInfo
        // 2.1 若仍是超时的, 则成功获取到锁, return true
        // 2.2 若不是超时的, 则进入下一次循环重新开始 步骤1
        // 3. lock没过期, 判断是否是当前线程持有
        // 3.1 是, 则计数加 1, return true
        // 3.2 否, 则进入下一次循环重新开始 步骤1
        // note: 每次进入循环都检查 : 1.是否超时, 若是则return false; 2.是否检查中断(interrupt)被中断,
        // 若需检查中断且被中断, 则抛InterruptedException
        while (!(useTimeout && isTimeout(start, timeout))) {
            if (interrupt) {
                checkInterruption();
            }

            if (tryLock()) {
                return true;
            }
        }

        locked = false;
        return false;
    }

    public String getLockKey() {
        return this.lockKey;
    }

    @Override
    public boolean tryLock() {
        // 锁超时时间
        long lockExpireTime = serverTimeMillis() + lockExpires + 1;
        LockInfo newLockInfo = LockInfo.newForCurrThread(lockExpireTime);
        // 条件能成立的唯一情况就是redis中lockKey还未关联value
        if (seeingLockRedisUtil.setnx(lockKey, newLockInfo) == 1) {
            // 成功获取到锁, 设置相关标识
            log.debug("{} setnx get lock(new), lockInfo: {}", Thread.currentThread().getId(), newLockInfo.toSimpleString());
            locked = true;
            return true;
        }

        // value已有值, 但不能说明锁被持有, 因为锁可能expired了
        LockInfo currLockInfo = seeingLockRedisUtil.get(lockKey);
        // 若这瞬间锁被delete了
        if (currLockInfo == null) {
            return false;
        }

        // 竞争条件只可能出现在锁超时的情况, 因为如果没有超时, 线程发现锁并不是被自己持有, 线程就不会去动value
        if (isTimeExpired(currLockInfo.getExpires())) {
            // 锁超时了
            Long oldExpires = seeingLockRedisUtil.getSetAnnex(lockKey, newLockInfo.getExpires());
            // 有多种情况，
            //  1、是返回null，就是被删了，这时如果 key 的 ttl 为 -1 那会造成永远无法获得 锁的情况。
            //  2、返回已经超时的那个
            //  3、返回的是已经被其它线程抢占的
            //  4、返回一个无法超时的时间，但其它线程并没有占用，这时 key 的 ttl 可能是 -1，也可能是用户没有 unlock
            if (oldExpires != null && isTimeExpired(oldExpires)) {
                // 成功获取到锁, 设置相关标识
                seeingLockRedisUtil.set(lockKey, newLockInfo);
                log.debug("{} getset get lock(new), lockInfo: {}", Thread.currentThread().getId(), newLockInfo.toSimpleString());
                locked = true;
                return true;
            } else {
                long expire = seeingLockRedisUtil.getExpire(lockKey);
                if (expire == -1) {
                    seeingLockRedisUtil.setDefaultExpire(lockKey);
                }
            }
        } else {
            // 锁未超时, 不会有竞争情况
            // 当前线程持有
            if (isHeldByCurrentThread(currLockInfo)) {
                // 成功获取到锁, 设置相关标识
                // 设置新的锁超时时间
                currLockInfo.setExpires(serverTimeMillis() + lockExpires + 1);
                currLockInfo.incCount();
                seeingLockRedisUtil.set(lockKey, currLockInfo);
                log.debug("{} get lock(inc), lockInfo: {}", Thread.currentThread().getId(), currLockInfo.toSimpleString());
                locked = true;
                return true;
            }
        }

        locked = false;
        return false;
    }

    @Override
    public Condition newCondition() {
        return null;
    }

    /**
     * Queries if this lock is held by any thread.
     *
     * @return {@code true} if any thread holds this lock and {@code false}
     * otherwise
     */
    public boolean isLocked() {
        // walkthrough
        // 1. lockKey未关联value, return false
        // 2. 若 lock 已过期, return false, 否则 return true
        if (!locked) {
            // 本地locked为false, 肯定没加锁
            return false;
        }
        LockInfo lockInfo = seeingLockRedisUtil.get(lockKey);
        if (lockInfo == null) {
            return false;
        }
        if (isTimeExpired(lockInfo.getExpires())) {
            return false;
        }
        return true;
    }

    @Override
    protected void unlock0() {
        // walkthrough
        // 1. 若锁过期, return
        // 2. 判断自己是否是锁的owner
        // 2.1 是, 若 count = 1, 则删除lockKey; 若 count > 1, 则计数减 1, return
        // 2.2 否, 则抛异常 IllegalMonitorStateException, reutrn
        // done, return

        LockInfo currLockInfo = seeingLockRedisUtil.get(lockKey);
        if (currLockInfo == null) {
            log.debug("current thread[{}] unlock, but lockInfo is null", Thread.currentThread().getId());
            return;
        }
        if (isTimeExpired(currLockInfo.getExpires())) {
            log.debug("current thread[{}] unlock, but lockInfo is expires", Thread.currentThread().getId());
            return;
        }

        if (isHeldByCurrentThread(currLockInfo)) {
            if (currLockInfo.getCount() == 1) {
                seeingLockRedisUtil.del(lockKey);
                log.debug("{} unlock(del), lockInfo: null", Thread.currentThread().getId());
            } else {
                currLockInfo.decCount(); // 持有锁计数减1
                seeingLockRedisUtil.set(lockKey, currLockInfo);
                log.debug("{} unlock(dec), lockInfo: {}", Thread.currentThread().getId(), currLockInfo.toSimpleString());
            }
        } else {
            log.error("current thread[{}] does not holds the lock, lockInfo:{}", Thread.currentThread().getId(), currLockInfo.toSimpleString());
            throw new IllegalMonitorStateException(String.format("current thread[%s] does not holds the lock, lockInfo:%s",
                    Thread.currentThread().getId(), currLockInfo.toSimpleString()));
        }
    }

    @Override
    public boolean isHeldByCurrentThread() {
        return isHeldByCurrentThread(seeingLockRedisUtil.get(lockKey));
    }

    // ------------------- utility methods ------------------------

    private boolean isHeldByCurrentThread(LockInfo lockInfo) {
        return lockInfo.isCurrentThread();
    }

    private void checkInterruption() throws InterruptedException {
        if (Thread.currentThread().isInterrupted()) {
            throw new InterruptedException();
        }
    }

    private boolean isTimeExpired(long time) {
        return time < serverTimeMillis();
    }

    private boolean isTimeout(long start, long timeout) {
        return start + timeout < System.currentTimeMillis();
    }

    private long serverTimeMillis() {
        return seeingLockRedisUtil.time();
    }

    private long localTimeMillis() {
        return System.currentTimeMillis();
    }

}
